Skip to content

Bound multipart text/plain field materialization in Jersey AppSec#11944

Open
jandro996 wants to merge 7 commits into
masterfrom
APMSP-3237
Open

Bound multipart text/plain field materialization in Jersey AppSec#11944
jandro996 wants to merge 7 commits into
masterfrom
APMSP-3237

Conversation

@jandro996

@jandro996 jandro996 commented Jul 14, 2026

Copy link
Copy Markdown
Member

What Does This Do

  • Replaces the unbounded bodyPart.getValue() call in MultiPartHelper.collectBodyPart (jersey2 and jersey3 AppSec modules) with readContent(bodyPart), which reads the part through the existing byte-capped MultipartContentDecoder (bounded by MAX_CONTENT_BYTES, backed by Config.get().getAppSecMaxFileContentBytes()).
  • Adds a distinct-field-name cap on the bodyMap collected from text/plain multipart fields, symmetric to the existing MAX_FILES_TO_INSPECT cap already applied to filesContent. Values for a field name already present in the map keep accumulating; only new distinct field names are capped once MAX_FILES_TO_INSPECT is reached.
  • Applies the identical change to both dd-java-agent/instrumentation/jersey/jersey-appsec/jersey-appsec-2.0 (javax.ws.rs) and jersey-appsec-3.0 (jakarta.ws.rs) — the two MultiPartHelper.java files remain byte-for-byte identical modulo the namespace.
  • Updates MultiPartHelperTest.groovy in both modules to mock bodyPart.getEntityAs(InputStream) instead of bodyPart.getValue() for the paths affected by the fix, and adds new test cases: truncation of a text/plain field value longer than MAX_CONTENT_BYTES, the field-name cap (MAX_FILES_TO_INSPECT) limiting distinct body-map keys, and accumulation of additional values for an already-seen field name even once the field-name cap is reached.

Motivation

Jersey 2/3 AppSec instrumentation materialized text/plain multipart field values via the unbounded getValue() and accumulated an unlimited number of distinct field names into the body map before any WAF limit was applied. A crafted multipart request with a very large text/plain field or a very large number of distinct field names could exhaust heap/CPU before hitting any bound, since neither content size nor field count was capped on this path.

Additional Notes

Scope is intentionally limited to Jersey. RESTEasy's MultipartFormDataReaderInstrumentation has a related unbounded-read pattern (getBodyAsString() on all parts) that is out of scope for this PR and should be addressed as a follow-up.

Contributor Checklist

Jira ticket: APMSP-3237

Note: Once your PR is ready to merge, add it to the merge queue by commenting /merge. /merge -c cancels the queue request. /merge -f --reason "reason" skips all merge queue checks; please use this judiciously, as some checks do not run at the PR-level. For more information, see this doc.

Jersey 2/3 AppSec instrumentation read text/plain multipart field
values via the unbounded getValue() and accumulated an unlimited
number of distinct field names, allowing a crafted multipart request
to exhaust heap/CPU before WAF limits apply. Reuse the existing
byte-capped MultipartContentDecoder and the configured file-content
limits (MAX_CONTENT_BYTES, MAX_FILES_TO_INSPECT) to bound both the
size of each field value and the number of distinct field names
collected.
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@jandro996 jandro996 added type: bug fix Bug fix comp: asm waf Application Security Management (WAF) labels Jul 14, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 124e9188ab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Jul 14, 2026

Copy link
Copy Markdown

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 69.73% (+12.53%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: d11f0a2 | Docs | Datadog PR Page | Give us feedback!

@dd-octo-sts

dd-octo-sts Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 13.99 s 13.98 s [-0.8%; +0.9%] (no difference)
startup:insecure-bank:tracing:Agent 12.90 s 12.98 s [-1.3%; +0.0%] (no difference)
startup:petclinic:appsec:Agent 16.98 s 16.87 s [-0.4%; +1.7%] (no difference)
startup:petclinic:iast:Agent 17.04 s 17.04 s [-0.8%; +0.7%] (no difference)
startup:petclinic:profiling:Agent 16.80 s 16.83 s [-1.3%; +0.9%] (no difference)
startup:petclinic:sca:Agent 17.02 s 16.77 s [+0.6%; +2.4%] (maybe worse)
startup:petclinic:tracing:Agent 15.76 s 16.11 s [-6.4%; +2.0%] (no difference)

Commit: d11f0a24 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

…d getName()

- Cap now bounds total accumulated values in the body map, not just distinct
  field names, closing a bypass where repeating a field name skipped the limit.
- Derive the body map key from FormDataContentDisposition.getName() (a safe
  field accessor) instead of FormDataBodyPart.getName(), which re-parses the
  disposition header and can throw on malformed input.

Addresses Codex review findings 1 and 3 on PR #11944.
…charset

Charset.defaultCharset() depends on the JVM/platform locale and can differ
from the UTF-8 default Jersey's own getValue() used, corrupting text
captured by AppSec on JVMs whose platform charset isn't UTF-8.

Addresses Codex review finding 2 on PR #11944.
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: e25666acf5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ultipartContentDecoder

MultipartContentDecoder is shared by Tomcat, Netty, Vert.x, RESTEasy and
commons-fileupload. Its Charset.defaultCharset() fallback was a deliberate,
reviewed decision (Manuel, PR #11198) - changing it in e25666a silently
altered production behavior for all those integrations, not just Jersey.

Revert the shared decoder change and instead default to UTF-8 only in
Jersey's MultiPartHelper, matching Jersey's own getValue() default, by
appending charset=UTF-8 to the contentType before calling the shared decoder.
@jandro996 jandro996 marked this pull request as ready for review July 14, 2026 13:28
@jandro996 jandro996 requested a review from a team as a code owner July 14, 2026 13:28
@jandro996 jandro996 requested review from ValentinZakharov, claponcet, manuel-alvarez-alvarez and vandonr and removed request for a team July 14, 2026 13:28

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06683b8439

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@datadog-datadog-us1-prod datadog-datadog-us1-prod Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datadog Autotest: PASS

More details

Validated charset defaulting and cap boundary: contentTypeWithDefaultUtf8 correctly appends UTF-8 when no charset is declared and preserves explicit charsets unchanged; the totalBodyMapValues cap blocks ALL additions once the total reaches MAX_FILES_TO_INSPECT — including repeated field names — which is more restrictive than the PR description's "distinct-field-name" framing, but intentional per the in-code comment and test assertions.

Was this helpful? React 👍 or 👎

📊 Validated against 28 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit 06683b8 · What is Autotest? · Any feedback? Reach out in #autotest

readContent() served both the bodyMap (text field) and filesContent
call sites, so the Jersey-local UTF-8 default leaked into file
content too. Split it into readContent() (UTF-8 default, text
fields) and readFileContent() (unmodified content type, parity with
MultipartContentDecoder's fallback used by file content in every
other multipart helper).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: asm waf Application Security Management (WAF) type: bug fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant